#define _GNU_SOURCE

#include <glib.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/poll.h>
#include <ctype.h>
#include <errno.h>
#include <netinet/tcp.h>
#include <arpa/telnet.h>

#include "ftp.h"

struct Ftp {
	int socket;
	char *host;
};

static int connection(const char *host, int port)
{
	int sfd = socket(PF_INET, SOCK_STREAM, 0);
	if(socket<0){
		perror("socket failed");
		return -1;
	}

	struct hostent* hostent = gethostbyname(host);
	if(!hostent){
		herror("gethostbyname failed");
		return -1;
	}

	struct sockaddr_in address = (struct sockaddr_in){
		.sin_family = AF_INET,
		.sin_addr.s_addr = *(int*)hostent->h_addr_list[0],
		.sin_port = htons(port),
	};

	int result = connect(sfd, (struct sockaddr*)&address, sizeof(address));
	if(result < 0){
		perror("connect failed");
		return -1;
	}

	return sfd;
}

static int waitfd(int fd, int ms)
{
	struct pollfd pfd = {fd, POLLIN|POLLPRI|POLLHUP|POLLRDHUP, 0};
	int poll_ret = poll(&pfd, 1, ms);
	if(poll_ret == 0 && ms)
		printf("Poll timeout\n"); 
	if(pfd.revents & (POLLHUP|POLLRDHUP)){
		fprintf(stderr, "Whee, ftp reset control connection!\n");
		exit(1);
	}
	return poll_ret == 1 ? pfd.revents : POLLERR;
}

static GString* readline(int fd)
{
	int readed;
	char c;
	GString *retval = g_string_new("");
	do{
		if((readed = read(fd, &c, 1)))
			g_string_append_c(retval, c);
	}while(readed > 0 && c!='\n' && c!=0);
	return retval;
}

static int response(Ftp *me, char** answer)
{
	char *line = NULL;
	int response = 0;

	while(waitfd(me->socket, 10*1000) & POLLIN){
		GString *string = readline(me->socket);
		line = string->str;
		if(string->len>0){
			response = strtol(line, NULL, 10);
			printf(line);
		}
		if(string->len > 3 && line[3] != '-')
			break;
		else
			g_string_free(string, TRUE);
	};
	
	if(answer)
		*answer = line;
	else
		g_free(line);

	return response/100;
}

Ftp* ftp_connect(const char *host)
{
	int answer = 0;
	Ftp* me = g_new0(Ftp, 1);
	me->host = g_strdup(host);
	
	//make socket
	me->socket = connection(host, 21);
	if(me->socket < 0){
		g_free(me);
		return NULL;
	}
	
	//set flags
	setsockopt(me->socket, IPPROTO_TCP, TCP_NODELAY, &(int){1}, sizeof(int));
	setsockopt(me->socket, SOL_SOCKET, SO_KEEPALIVE, &(int){1}, sizeof(int));

	//read server greeting
	response(me,0);
	

	//send user, pass 
	char user[] = "USER anonymous\n";
	char pass[] = "PASS anonymous@example.com\n";
	write(me->socket, user, strlen(user));
	answer = response(me, 0);
	if(answer != 2 && answer != 3){
		fprintf(stderr, "USER failed\n");
		return NULL;
	}

	write(me->socket, pass, strlen(pass));
	answer = response(me, 0);
	if(answer != 2){
		fprintf(stderr, "PASS failed\n");
		return NULL;
	}
	return me;
}

static int open_data_connection(Ftp *me, const char* fmt, ...)
{
	char *pasv_answer = NULL;
	char pasv[] = "PASV\n";
	write(me->socket, pasv, strlen(pasv));
	int ecode = response(me, &pasv_answer);
	if(ecode != 2){
		fprintf(stderr, "PASV failed\n");
		return -1;
	}

	char *port_b = pasv_answer+3;
	while(!isdigit(*port_b))
		port_b++;

	int port_h, port_l, port;
	ecode = sscanf(port_b, "%*d,%*d,%*d,%*d,%d,%d)", &port_h, &port_l);
	if(ecode != 2){
		fprintf(stderr, "Can't parse PASV answer\n");
		return -1;
	}
	port = port_h*256 + port_l;

	int data_sfd = connection(me->host, port);
	if(data_sfd < 0){
		return data_sfd;
	}

	va_list ar;
	va_start(ar, fmt);
	char *cmd = g_strdup_vprintf(fmt, ar);
	printf("< %s", cmd);
	write(me->socket, cmd, strlen(cmd));
	ecode = response(me, 0);
	g_free(cmd);
	if(ecode == 4 || ecode == 5){
		fprintf(stderr, "Response to LIST is not around 100\n");
		return -1;
	}

	setsockopt(me->socket, SOL_SOCKET, SO_LINGER, &(struct linger){1,5}, sizeof(struct linger));

	return data_sfd;
}

int ftp_list(Ftp *me, const char *path)
{
	return open_data_connection(me, "LIST %s\n", path);
}

int ftp_close(Ftp *me, int data_fd)
{
	close(data_fd);
	return response(me, 0);
}

void ftp_destroy(Ftp* me)
{
	g_free(me->host);
	close(me->socket);
}

int ftp_retr(Ftp* me, const char *path, off_t offset)
{
	char *type_cmd = "TYPE I\n";
	write(me->socket, type_cmd, strlen(type_cmd));
	if(response(me, 0) != 2){
		fprintf(stderr, "TYPE I failed\n");
		return -1;
	}

	if(offset){
		char *rest_cmd = g_strdup_printf("REST %lld\n", offset);
		write(me->socket, rest_cmd, strlen(rest_cmd));
		g_free(rest_cmd);
		if(response(me, 0) != 3){
			fprintf(stderr, "REST failed\n");
			return -1;
		}
	}

	return open_data_connection(me, "RETR %s\n", path);
}
